package com.smap;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.dom.client.CanvasElement;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.HumanInputEvent;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.dom.client.TouchEvent;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.ProvidesResize;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.Widget;
import com.smap.common.MouseAdapter;
import com.smap.common.ResourceLoader;
import com.smap.common.TouchAdapter;
import com.smap.common.Utils;
import com.smap.common.Utils.AnimateCallback;
import com.smap.control.ToolBar;
import com.smap.core.Bbox;
import com.smap.core.Control;
import com.smap.core.Layer;
import com.smap.core.Map;
import com.smap.core.MapStatus;
import com.smap.core.Marker;
import com.smap.core.Projection;
import com.smap.core.XY;
import com.smap.event.MapEvent;
import com.smap.event.MapHandler;
import com.smap.layer.CanvasLayer;
import com.smap.proj.SphericalMercator;

/**
 * A rich map widget
 * 
 * @author Shen
 */
public class SMap extends Composite implements Map, RequiresResize,
		ProvidesResize, MapHandler {

	/**
	 * maxExtent of the whole map;
	 */
	public Bbox maxExtent = new Bbox();
	/**
	 * map projection, a map always has one projection
	 */
	public Projection proj;

	private double maxResolution = 0;
	private boolean isLoaded = false;
	private List<Control> controls = new ArrayList<Control>();
	private List<Layer> layers = new ArrayList<Layer>();
	private List<Marker> markers = new ArrayList<Marker>();

	private XY centre;
	private int zoom;
	private int width = 0;
	private int height = 0;
	private AbsolutePanel body;

	private AbsolutePanel eventPanel;
	private Canvas screenBuffer;
	private AbsolutePanel root;
	private AbsolutePanel rootFront;

	public int getZoom() {
		return zoom;
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	public Context2d getScreen() {
		return screenBuffer.getContext2d();
	}

	public XY getCentre() {
		return centre;
	}

	public List<Layer> getLayers() {
		return layers;
	}

	public AbsolutePanel getRoot() {
		return root;
	}

	MouseAdapter mouseAdapter;
	TouchAdapter touchAdapter;

	public SMap() {
		RS.INSTANCE.style().ensureInjected();
		initDom();
		initParams();
	}

	@SuppressWarnings("rawtypes")
	private void fireNativeEvent(HumanInputEvent evt) {
		try {
			MapEvent event = MapEvent.convert(evt);
			if (event != null) {
				event.x = event.x - SMap.this.width / 2;
				event.y = event.y - SMap.this.height / 2;
				SMap.this.fireEvent(event);
			}
			evt.stopPropagation();
			evt.preventDefault();
		} catch (Exception e) {

		}
	}

	private void initDom() {
		body = new AbsolutePanel();
		initWidget(body);
		this.setStyleName("SMap");

		root = new AbsolutePanel();
		root.setSize("0px", "0px");
		Style style = root.getElement().getStyle();
		style.setOverflow(Overflow.VISIBLE);
		body.add(root);

		screenBuffer = Canvas.createIfSupported();
		screenBuffer.setStyleName("screenBuffer");
		body.add(screenBuffer, 0, 0);

		eventPanel = new AbsolutePanel();
		eventPanel.setStyleName("eventPanel");
		Style s = eventPanel.getElement().getStyle();
		s.setBackgroundColor("red");
		s.setOpacity(0);
		s.setZIndex(1000);
		body.add(eventPanel, 0, 0);

		rootFront = new AbsolutePanel();
		rootFront.setSize("0px", "0px");
		body.add(rootFront);
		Style s2 = rootFront.getElement().getStyle();
		s2.setOverflow(Overflow.VISIBLE);
		s2.setZIndex(1001);

		mouseAdapter = new MouseAdapter() {
			@Override
			public void onEvent(@SuppressWarnings("rawtypes") MouseEvent evt) {
				fireNativeEvent(evt);
			}
		};
		mouseAdapter.install(eventPanel);
		touchAdapter = new TouchAdapter() {
			@SuppressWarnings("rawtypes")
			@Override
			public void onEvent(TouchEvent evt) {
				if (mouseAdapter != null) {
					mouseAdapter.remove();
					mouseAdapter = null;
				}
				fireNativeEvent(evt);
			}
		};
		touchAdapter.install(eventPanel);

		toolBar = new ToolBar(this);
		this.add(toolBar, 10, 10);
	}

	public void initParams() {
		this.proj = new SphericalMercator();
		this.maxResolution = proj.getMaxResolution();
		setMaxExtent("-180,-90,180,90");
	}

	public void setMaxExtent(String ps) {
		String[] p = ps.split(",");
		setMaxExtent(
				new XY(Double.parseDouble(p[0]), Double.parseDouble(p[1])),
				new XY(Double.parseDouble(p[2]), Double.parseDouble(p[3])),
				true);
	}

	public void setMaxExtent(XY p1, XY p2, boolean needProject) {
		if (needProject) {
			this.proj.project(p1);
			this.proj.project(p2);
		}
		this.maxExtent.clear();
		this.maxExtent.expand(p1).expand(p2);
	}

	public void setRoot(int x, int y) {
		body.setWidgetPosition(root, x, y);
		body.setWidgetPosition(rootFront, x, y);
	}

	public void setScreenOffset(int offsetX, int offsetY) {
		int x = width / 2;
		int y = height / 2;
		setRoot(x + offsetX, y + offsetY);
	}

	public void zoom(XY coord, Integer zoom) {
		this.zoom(coord, zoom, false);
	}

	public void zoom(XY coord, Integer zoom, boolean needProject) {
		if (coord != null) {
			if (needProject) {
				proj.project(coord);
			}
			this.centre = coord;
		}
		if (zoom != null) {
			this.zoom = zoom;
		}

		this.draw(true);
	}

	public void zoomByLonLat(Double x, Double y, Integer zoom) {
		this.zoom(new XY(x, y), zoom, true);
	}

	MapStatus lastDraw;

	public void draw(boolean effect) {
		if (!isLoaded) {
			return;
		}
		ResourceLoader.get().clear();
		MapStatus status = new MapStatus(centre.x, centre.y, zoom);
		if (effect && (lastDraw != null)) {
			showEffect(status);
		}
		// layers
		for (Layer layer : layers) {
			layer.draw();
		}
		// markers
		for (Marker marker : markers) {
			marker.draw();
		}
		fireEvent(new MapEvent(MapEvent.EVT_MAP_DRAWN, status));
		lastDraw = status;
	}

	private void showEffect(MapStatus status) {

		if (lastDraw.x != status.x && lastDraw.y != status.y) {
			final XY change = mapToPixel(new XY(lastDraw.x, lastDraw.y));
			// System.out.println(change);
			if ((Math.abs(change.x) + Math.abs(change.y)) < 1000) {
				showTempScreen(true);
				double speed = (Math.abs(change.x) + Math.abs(change.y)) / 1000.0;
				speed = Math.max(speed, 1);
				speed = Math.min(speed, 5);

				Utils.animate(change, speed, new AnimateCallback() {
					public void finish() {
						showTempScreen(false);
						setRoot(width / 2, height / 2);
					}

					public void step(Double x, Double y) {
						Utils.setPosition(screenBuffer, x.intValue(),
								y.intValue());
						int x2 = (int) (change.x - x);
						int y2 = (int) (change.y - y);
						setRoot(width / 2 - x2, height / 2 - y2);
					}
				});
			}
		} else {
			double zoomChange = Math.pow(2, status.zoom - lastDraw.zoom);
			if (zoomChange != 1) {
				showTempScreen(true);
				Utils.animate(1, zoomChange, new AnimateCallback() {
					public void finish() {
						showTempScreen(false);
					}

					public void step(Double x, Double nothing) {
						screenBuffer.setSize(width * x + "px", height * x
								+ "px");
						int left = (int) (width * (1 - x));
						int top = (int) (height * (1 - x));
						Utils.setPosition(screenBuffer, left / 2, top / 2);
					}
				});
			}
		}
	}

	public void addLayer(Layer layer) {
		layers.add(layer);
		layer.install(this);
	}

	public void addControl(Control c) {
		controls.add(c);
		c.install(this);
	}

	@Override
	protected void onLoad() {
		super.onLoad();
		for (Control control : controls) {
			control.activate();
		}
		if (width == 0) {
			new Timer() {
				@Override
				public void run() {
					onResize();
				}
			}.schedule(100);
		}
		isLoaded = true;

		this.addHandler(this, MapEvent.TYPE);
	}

	@Override
	protected void onUnload() {
		for (Control control : controls) {
			control.deActivate();
		}
		super.onUnload();
		isLoaded = false;
	}

	public void setCursor(String name) {
		String cursorName = "default";
		if (name != null) {
			cursorName = name;
		}
		DOM.setStyleAttribute(this.body.getElement(), "cursor", cursorName);
	}

	public void addMarker(Marker marker) {
		marker.install(this);
		markers.add(marker);
	}

	public void onResize() {
		int w = this.getOffsetWidth();
		int h = this.getOffsetHeight();
		// System.out.println(w + "," + h);
		if ((this.width == w) && (this.height == h)) {
			return;
		}
		this.width = w;
		this.height = h;
		setScreenOffset(0, 0);
		for (Layer layer : layers) {
			layer.resize(w, h);
		}
		// System.out.println("set" + w + "," + h);
		Utils.setCanvasSize(screenBuffer, w, h);
		eventPanel.setSize(w + "px", h + "px");
		for (Control control : controls) {
			control.onResize();
		}
		draw(false);
	}

	public void zoomIn() {
		zoom++;
		draw(true);
	}

	public void zoomOut() {
		zoom--;
		draw(true);
	}

	/**
	 * @param point
	 *            screen pixel , screen centre is 0,0
	 * @return
	 */
	public XY pixelToMap(XY pixel) {
		double res = getResolution();
		XY mapPoint = this.centre.clone();
		mapPoint.x += pixel.x * res;
		mapPoint.y -= pixel.y * res;
		return mapPoint;
	}

	public XY mapToPixel(XY coord) {
		double res = getResolution(zoom);
		XY pixel = new XY(coord.x - this.centre.x, coord.y - this.centre.y);
		pixel.x = pixel.x / res;
		pixel.y = -pixel.y / res;
		return pixel;
	}

	public void updateLocation() {
		int changeX = -body.getWidgetLeft(root) + width / 2;
		int changeY = -body.getWidgetTop(root) + height / 2;
		this.centre = pixelToMap(new XY(changeX, changeY));
		// show a buffer to look good
		showTempScreen(true);
		new Timer() {
			@Override
			public void run() {
				showTempScreen(false);
			}
		}.schedule(500);
		setScreenOffset(0, 0);
		draw(false);
	}

	private void clearScreen() {
		Utils.setCanvasSize(screenBuffer, width, height);
		Context2d ctx = screenBuffer.getContext2d();
		ctx.clearRect(0, 0, width, height);
		screenBuffer.getElement().getStyle().setTop(0, Unit.PX);
		screenBuffer.getElement().getStyle().setLeft(0, Unit.PX);
	}

	private void showTempScreen(boolean isShow) {
		if (isShow) {
			XY offset = new XY(body.getWidgetLeft(root) - width / 2,
					body.getWidgetTop(root) - height / 2);
			for (Layer layer : layers) {
				if (layer instanceof CanvasLayer) {
					CanvasLayer cLayer = (CanvasLayer) layer;
					CanvasElement ce = cLayer.getCanvas().getCanvasElement();
					XY offset2 = cLayer.getCanvasOffset();
					XY drawStart = new XY(width / 2, height / 2);
					drawStart.move(offset2).move(offset);
					screenBuffer.getContext2d().drawImage(ce, drawStart.x,
							drawStart.y);
				}
			}
			for (Layer layer : layers) {
				layer.setVisibility(false);
			}
		} else {
			clearScreen();
			for (Layer layer : layers) {
				layer.setVisibility(true);
			}
		}
	}

	public void switchBaseMap() {
		Layer layer = this.layers.get(0);
		layer.setType(null);
		draw(false);
	}

	public Bbox getExtent() {
		return getExtent(this.width, this.height);
	}

	public Bbox getExtent(int w, int h) {
		Bbox bbox = new Bbox();
		XY theCentre = this.centre;
		double res = getResolution(this.zoom);
		double changeX = w / 2 * res;
		double changeY = h / 2 * res;
		bbox.expand(theCentre.x - changeX, theCentre.y - changeY);
		bbox.expand(theCentre.x + changeX, theCentre.y + changeY);
		return bbox;
	}

	public double getResolution() {
		return getResolution(this.zoom);
	}

	int lastZoom = -1;
	double lastResolution = 0;

	public double getResolution(int zoom) {
		if (zoom != lastZoom) {
			lastResolution = maxResolution / Math.pow(2, zoom);
			lastZoom = zoom;
		}
		return lastResolution;
	}

	/**
	 * zoom to a screen rectangle
	 * 
	 * @param rect
	 */
	public void zoomTo(int[] rect) {
		// centre
		int x = rect[0] + rect[2] / 2;
		int y = rect[1] + rect[3] / 2;
		XY newCentre = pixelToMap(new XY(x, y));
		// zoom
		int times = this.width / rect[2];
		int zoomChange = (int) Math.pow(times, 1d / 2);
		zoom(newCentre, zoom + zoomChange);
	}

	public void add(Widget widget, int x, int y) {
		body.add(widget, x, y);
	}

	public Projection getProj() {
		return proj;
	}

	public List<Marker> getMarkers() {
		return markers;
	}

	public AbsolutePanel getRootFront() {
		return rootFront;
	}

	public void command(String cmd) {
		List<Control> rest = new ArrayList<Control>();
		String group = null;
		for (Control control : controls) {
			if (control.acceptCmd(cmd)) {
				control.activate(cmd);
				toolBar.update(control);
				group = control.getGroup();
			} else {
				rest.add(control);
			}
		}
		if (group != null)
			for (Control control : rest) {
				if (group.equals(control.getGroup())) {
					control.deActivate();
					toolBar.update(control);
				}
			}
	}

	ToolBar toolBar;

	public ToolBar getToolBar() {
		return toolBar;
	}

	public void onMapEvent(MapEvent mapEvent) {
		for (int i = controls.size() - 1; i >= 0; i--) {
			controls.get(i).onMapEvent(mapEvent);
		}
	}

}
